package cn.seqdata.jdbc;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;

import org.apache.commons.lang3.ObjectUtils;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.springframework.data.domain.Persistable;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;

/**
 * <blockquote>
 *
 * <pre>
 * JpaRowMapper&lt;ID, T&gt; mapper = new JpaRowMapper&lt;&gt;(ID.class, T.class);
 * List&lt;T&gt; entities = jdbcTemplate.query(sql, mapper);
 * </pre>
 *
 * </blockquote>
 */
public class JpaRowMapper<ID extends Serializable, T extends Persistable<ID>> implements RowMapper<T> {
	public static final DefaultFormattingConversionService cs = new DefaultFormattingConversionService();
	private final BeanPropertyRowMapper<T> rowMapper;

	private final Class<ID> idClass;
	private final Class<T> klass;

	static {
		cs.addConverter(Timestamp.class, DateTime.class, JodaUtils::toDateTime);
		cs.addConverter(Date.class, LocalDate.class, JodaUtils::toLocalDate);
		cs.addConverter(Time.class, LocalTime.class, JodaUtils::toLocalTime);
	}

	public JpaRowMapper(Class<ID> idClass, Class<T> klass) {
		super();

		this.idClass = idClass;
		this.klass = klass;

		rowMapper = new BeanPropertyRowMapper<>(klass);
		rowMapper.setConversionService(cs);
	}

	@Override
	public T mapRow(ResultSet rs, int rowNum) throws SQLException {
		T entity = rowMapper.mapRow(rs, rowNum);

		try {
			Method setIdMethod = klass.getMethod("setId", idClass);
			if(Objects.nonNull(setIdMethod)) {
				setIdMethod.invoke(entity, mapID(rs, rowNum));
			}
		} catch(ReflectiveOperationException ex) {
			throw new SQLException(ex);
		}

		return entity;
	}

	@SuppressWarnings("unchecked")
	private ID mapID(ResultSet rs, int rowNum) throws SQLException {
		try {
			Method getIdMethod = klass.getMethod("getId");
			if(getIdMethod.isAnnotationPresent(EmbeddedId.class)) {
				BeanPropertyRowMapper<ID> idMapper = new BeanPropertyRowMapper<>(idClass);
				return idMapper.mapRow(rs, rowNum);
			} else if(getIdMethod.isAnnotationPresent(Id.class)) {
				Column c = getIdMethod.getAnnotation(Column.class);
				String field = ObjectUtils.defaultIfNull(c.name(), "id");
				Object obj = rs.getObject(field);
				if(Integer.class.equals(idClass)) {
					if(obj instanceof Number) {
						int val = ((Number) obj).intValue();
						return (ID) Integer.valueOf(val);
					}
				} else if(Long.class.equals(idClass)) {
					if(obj instanceof Number) {
						long val = ((Number) obj).longValue();
						return (ID) Long.valueOf(val);
					}
				} else if(String.class.equals(idClass)) {
					return (ID) String.valueOf(obj);
				}
				return (ID) obj;
			} else {
				return rs.getObject(1, idClass);
			}
		} catch(ReflectiveOperationException ex) {
			throw new SQLException(ex);
		}
	}
}